Skip to content

Fix: Finalize swept pending tx#999

Merged
altafan merged 16 commits intoarkade-os:masterfrom
louisinger:finalize-swept-pending-tx
Apr 10, 2026
Merged

Fix: Finalize swept pending tx#999
altafan merged 16 commits intoarkade-os:masterfrom
louisinger:finalize-swept-pending-tx

Conversation

@louisinger
Copy link
Copy Markdown
Collaborator

@louisinger louisinger commented Apr 1, 2026

we need 2 test cases :

  • finalize a pending tx after it has been swept by the server
  • finalize a pending tx after it has expired but was not swept by the server

@altafan please review

Summary by CodeRabbit

  • Documentation

    • Updated ARKD_VTXO_TREE_EXPIRY docs with regtest constraint; removed docs for deprecated env vars.
  • Bug Fixes

    • More accurate detection and reporting of swept vtxos for finalized offchain transactions.
  • Chores

    • Removed explicit scheduler and CSV-block config options; scheduler chosen from locktime/expiry settings.
    • Block-based relative locktimes now interpreted on a seconds scale.
  • Tests

    • Added end-to-end tests for finalized swept transaction scenarios.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Scheduler config fields removed; scheduler selection now derives from VtxoTreeExpiry.Type. Vtxo swept-status logic updated to consult round/batch sweep state and offchain tx expiry when finalizing transactions. Tests updated with new finalize-swept scenarios and reduced sleeps. Block-to-seconds conversion constant changed to 1.

Changes

Cohort / File(s) Summary
Configuration System Refactoring
internal/config/config.go, docker-compose.regtest.yml, envs/arkd.dev.env, envs/arkd.light.env, README.md
Removed SchedulerType and AllowCSVBlockType config fields/env vars. Scheduler instantiation now chosen by VtxoTreeExpiry.Type. Added network-gated validation disallowing block-based locktimes outside regtest. Updated envs and README (expiry/value notes, added ARKD_CHECKPOINT_EXIT_DELAY).
Vtxo Swept-Status Logic
internal/infrastructure/db/service.go, internal/core/application/service.go
When finalizing offchain txs, determine txSwept by loading round via RootCommitmentTxId, checking batch SweepTxs, and ExpiryTimestamp. New vtxos inherit Swept=true if txSwept; otherwise fallback to prior dust/subdust checks. Event handler marks new vtxos swept if spent vtxos are swept/expired.
Tests & Timing
internal/test/e2e/e2e_test.go
Added finalize pending swept tx subtest with “vtxo already swept” and “vtxo expired but not swept” scenarios. Adjusted multiple sleep durations across e2e tests.
Locktime Conversion Constant
pkg/ark-lib/locktime.go
Changed SECONDS_PER_BLOCK from 10 * 60 to 1, so block-based relative locktimes are interpreted as seconds.

Sequence Diagram(s)

sequenceDiagram
    participant OffchainTxHandler as "OffchainTx Handler\n(app)"
    participant DB as "DB Service\n(load round/batch)"
    participant Scheduler as "Scheduler\n(current time)"
    participant Indexer as "Indexer/Notifier"

    OffchainTxHandler->>DB: Load round by RootCommitmentTxId
    DB-->>OffchainTxHandler: round or error (batch info)
    OffchainTxHandler->>Scheduler: Query current time/state
    Scheduler-->>OffchainTxHandler: now (for expiry checks)
    OffchainTxHandler->>OffchainTxHandler: compute txSwept = (batch.SweepTxs exist) OR (load error treated swept) OR (ExpiryTimestamp < now)
    OffchainTxHandler->>DB: create new vtxos with Swept = txSwept OR fallback(dust check)
    DB-->>Indexer: emit TransactionEvent (with SpentVtxos, NewVtxos, Swept flags)
    Indexer-->>Clients: notify (NotifyIncomingFunds etc.)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix: Finalize swept pending tx' accurately summarizes the main change: handling finalization of swept pending transactions with related configuration and locktime adjustments.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@louisinger louisinger requested a review from altafan April 1, 2026 14:53
@arkanaai
Copy link
Copy Markdown
Contributor

arkanaai bot commented Apr 1, 2026

🔍 Review — #999

Fix: Finalize swept pending tx

Summary

When an offchain tx is finalized after its expiry, the new VTXOs may need to be marked as swept if the inputs were already swept by the server. Previously the code unconditionally set Swept based only on the dust-script check (IsSubDustScript). This PR adds input-inspection logic to propagate the swept status from inputs to outputs.

What Changed

Single file: internal/infrastructure/db/service.go

  1. Captures ins from DecodeTx — previously discarded with _, now used to look up input VTXOs when the tx is expired at finalization.

  2. Swept propagation logic (lines 536-550):

    • Only triggers when offchainTx.ExpiryTimestamp > 0 AND time.Now() is past expiry
    • Fetches input VTXOs via s.vtxoStore.GetVtxos(ctx, ins)
    • If any input VTXO has Swept == true, all new outputs are marked swept
    • If the store returns an error, defaults to txSwept = true (safe fallback — avoids creating spendable VTXOs from potentially swept inputs)
  3. Output swept assignment (lines 568-571):

    • outputSwept is true if either txSwept (input was swept) OR IsSubDustScript(out.PkScript) (sub-dust output)
    • Replaces the old isDust variable

Security Analysis

✅ Safe fallback on error: txSwept = err != nil means if the VTXO store lookup fails, outputs are conservatively marked swept. This prevents a scenario where a DB error could create spendable VTXOs from swept inputs.

✅ Correct swept propagation: If the server swept the VTXO tree (inputs are swept), the outputs of a finalized tx built from those inputs should also be swept — they exist in the same expired tree. This is the right behavior.

⚠️ time.Now() in projection update: Using wall-clock time is reasonable here since this runs during event processing, but worth noting that in tests with mocked time this could behave differently. Not a blocker.

⚠️ Any-input-swept semantics: If a tx has multiple inputs and only one is swept, ALL outputs are marked swept. This is conservative and correct — if any input came from a swept tree, the outputs derived from it cannot be independently settled.

Missing test coverage

The PR description mentions two needed test cases:

  1. Finalize a pending tx after it has been swept by the server
  2. Finalize a pending tx after it has expired but was NOT swept by the server

Neither is included in this PR. These would be valuable to add, especially the second case which verifies that non-swept expired txs produce spendable outputs.

Verdict

Clean, minimal fix with correct safety semantics. The conservative error handling is the right call. Tests should follow to cover both swept and non-swept expiry scenarios.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/infrastructure/db/service.go`:
- Around line 541-543: The code currently sets txSwept = err != nil after
calling s.vtxoStore.GetVtxos, which incorrectly treats DB read errors as
permanent swept state; remove the assignment and instead handle GetVtxos errors
explicitly (e.g., return the error up, log and skip processing, or retry) so
transient read failures don't mark vtxos swept; determine txSwept only from the
actual inputVtxos result (or explicit spent flags) after a successful call to
s.vtxoStore.GetVtxos rather than based on err.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1cf6a5f5-cea2-48d9-b80f-15ff544c4aea

📥 Commits

Reviewing files that changed from the base of the PR and between f924dd4 and 91781e2.

📒 Files selected for processing (1)
  • internal/infrastructure/db/service.go

Comment thread internal/infrastructure/db/service.go Outdated
@bitcoin-coder-bob
Copy link
Copy Markdown
Collaborator

@louisinger I added the 2 test cases you outlined in the PR description

@arkanaai
Copy link
Copy Markdown
Contributor

arkanaai bot commented Apr 2, 2026

Iterative review — new commit 3890387a (test additions)

Good test coverage for the swept-status propagation logic. Two cases are the right ones to test:

  1. ✅ Input swept → output swept (server already reclaimed)
  2. ✅ Input not swept → output not swept (expired but unclaimed)

Observations:

  • The mockTxDecoder is shared across subtests but decodeFn is reassigned per subtest — works here because subtests run sequentially, but worth noting if parallelism is ever added.

  • taprootPkScript silently discards the error from hex.DecodeString. Fine for test code with a known-good constant, but a t.Fatal would be more defensive.

  • The require.Eventually with 5s timeout / 100ms poll is appropriate for the async projection handler. The test correctly re-fetches after the poll to get a clean assertion.

  • The mock decoder is defined at the outer scope but the decodeFn closure captures outTxid, in1, in2 from the inner subtest scope — this is correct but subtle. The comment "swapped per-subtest" helps.

Overall this is solid test coverage for a critical edge case. LGTM on the test additions. The core logic in service.go was reviewed in the prior pass and the swept-propagation approach is sound.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/infrastructure/db/service_test.go (1)

2201-2206: Preserve failure cause in async polling.

If projection fails internally, this eventually check times out without surfacing the last DB error, which makes failures harder to triage.

💡 Proposed fix
+			var lastErr error
 			require.Eventually(t, func() bool {
 				vtxos, err := svc.Vtxos().GetVtxos(ctx, []domain.Outpoint{
 					{Txid: outTxid, VOut: 0},
 				})
-				return err == nil && len(vtxos) > 0
-			}, 5*time.Second, 100*time.Millisecond)
+				lastErr = err
+				return err == nil && len(vtxos) > 0
+			}, 5*time.Second, 100*time.Millisecond,
+				"projected output vtxo not found (last error: %v)", lastErr)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/infrastructure/db/service_test.go` around lines 2201 - 2206, The
test's require.Eventually call hides the underlying error from
svc.Vtxos().GetVtxos when the predicate keeps returning false; capture the last
error inside the polling closure (e.g., a variable lastErr set to err each
iteration) and after require.Eventually returns assert or fail with that
captured error so the real DB/projection failure is surfaced; modify the test
around the require.Eventually invocation that calls svc.Vtxos().GetVtxos to
record the last err and then use require.NoError(lastErr) or t.Fatalf with
lastErr if the eventually check still fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/infrastructure/db/service_test.go`:
- Around line 2036-2042: taprootPkScript currently ignores hex.DecodeString
errors and doesn't validate the decoded key length, which lets malformed fixture
hex produce truncated/zero-padded 34-byte scripts; change taprootPkScript to
validate input by handling the hex.DecodeString error and verifying len(pk) ==
32, and fail fast on invalid input (preferably by accepting a *testing.T
parameter and calling t.Fatalf with a clear message, or return an error) before
constructing the 34-byte script so malformed fixtures cause test failures rather
than silent false positives.

---

Nitpick comments:
In `@internal/infrastructure/db/service_test.go`:
- Around line 2201-2206: The test's require.Eventually call hides the underlying
error from svc.Vtxos().GetVtxos when the predicate keeps returning false;
capture the last error inside the polling closure (e.g., a variable lastErr set
to err each iteration) and after require.Eventually returns assert or fail with
that captured error so the real DB/projection failure is surfaced; modify the
test around the require.Eventually invocation that calls svc.Vtxos().GetVtxos to
record the last err and then use require.NoError(lastErr) or t.Fatalf with
lastErr if the eventually check still fails.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5d9b02fb-1af7-439d-8e20-aa647acb2151

📥 Commits

Reviewing files that changed from the base of the PR and between 91781e2 and 3890387.

📒 Files selected for processing (1)
  • internal/infrastructure/db/service_test.go

Comment thread internal/infrastructure/db/service_test.go Outdated
@arkanaai
Copy link
Copy Markdown
Contributor

arkanaai bot commented Apr 2, 2026

Iterative review — new commit 088acf5

Minor test hardening: taprootPkScript now takes *testing.T, validates hex decode errors, and asserts 32-byte key length. Good practice — silent _ on decode errors could mask bad test data.

LGTM for this increment. No concerns.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/infrastructure/db/service_test.go (1)

2072-2074: Prefer per-subtest decoder instance to avoid shared mutable state.

Creating one decoder outside the loop makes subtests depend on mutable shared state. Instantiating it inside each t.Run keeps isolation tight and future-proofs for t.Parallel().

♻️ Suggested refactor
-	// The mock decoder is swapped per-subtest to return unique outpoints.
-	decoder := &mockTxDecoder{}
-
 	tests := []struct {
 		name        string
 		inputSwept  bool
 		expectSwept bool
@@
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
+			decoder := &mockTxDecoder{}
 			// Fresh db per subtest for clean isolation.
 			dbDir := t.TempDir()
 			svc, err := db.NewService(db.ServiceConfig{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/infrastructure/db/service_test.go` around lines 2072 - 2074, The
test creates a single shared decoder variable (decoder := &mockTxDecoder{})
outside the subtest loop which introduces shared mutable state; change the code
to instantiate a fresh mockTxDecoder inside each t.Run (e.g., inside the subtest
closure create decoder := &mockTxDecoder{}) so each subtest gets its own decoder
instance, update any references in the subtest to use that local decoder, and
ensure this enables safe use of t.Parallel() in the subtests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/infrastructure/db/service_test.go`:
- Around line 2122-2126: The mock decode function (decoder.decodeFn) currently
ignores its tx argument which can hide wiring bugs; update decoder.decodeFn to
assert the incoming tx equals the expected transaction payload used in the test
(e.g., compare to the test's raw/serialized tx variable) and fail the test if it
differs (using the test's t.Fatalf/t.Helper or require/assert helper). Keep the
returned outTxid, in1, in2 and outputs unchanged but add that assertion at the
start of decoder.decodeFn so wrong payloads cause a visible test failure.

---

Nitpick comments:
In `@internal/infrastructure/db/service_test.go`:
- Around line 2072-2074: The test creates a single shared decoder variable
(decoder := &mockTxDecoder{}) outside the subtest loop which introduces shared
mutable state; change the code to instantiate a fresh mockTxDecoder inside each
t.Run (e.g., inside the subtest closure create decoder := &mockTxDecoder{}) so
each subtest gets its own decoder instance, update any references in the subtest
to use that local decoder, and ensure this enables safe use of t.Parallel() in
the subtests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bc5c8405-9aa8-45a8-a183-31c4b29ec4fa

📥 Commits

Reviewing files that changed from the base of the PR and between 3890387 and 088acf5.

📒 Files selected for processing (1)
  • internal/infrastructure/db/service_test.go

Comment thread internal/infrastructure/db/service_test.go Outdated
@arkanaai
Copy link
Copy Markdown
Contributor

arkanaai bot commented Apr 3, 2026

🔍 PR Review: Fix Finalize Swept Pending TX

TL;DR: Solid bug fix. Prevents swept/expired input VTXOs from producing spendable output VTXOs during offchain tx finalization.

What Changed

service.go — When an offchain tx is finalized after its expiry, the fix now:

  1. Decodes inputs (previously discarded with _)
  2. Checks if any input VTXO is swept via GetVtxos
  3. Propagates swept status to all output VTXOs

Analysis

The bug: If a pending offchain tx was finalized after its inputs were swept (or expired past their timelock), the new output VTXOs were created without the Swept flag. This could allow spending funds that the server has already swept on-chain — a double-spend vector.

The fix logic is sound:

  • txSwept = err != nil — If the DB lookup fails, conservatively assume swept. Correct trade-off: false-positive swept is safe (funds locked, recoverable via settlement), false-negative would be a security hole.
  • Any single swept input marks all outputs swept — correct, since the VTXO tree is atomically swept on-chain.
  • Dust check preserved as fallback: outputSwept = txSwept || IsSubDustScript(...).

One consideration

The time.Now() comparison against ExpiryTimestamp is straightforward but creates a small window: what about the "expired but not yet swept" case?

Looking at the test: it generates 21 blocks (expiring the VTXO) without waiting for the server sweep. The input VTXO is Spent: true, Swept: false. When finalization runs, GetVtxos(ctx, ins) returns the input with Swept: false, so txSwept stays false. But the test expects the output to be Swept: true — which means either:

  • There is additional server-side logic during finalization not shown in this diff, or
  • This test case might fail as written

Could you clarify the expected path for "expired but not yet swept"? The code in this diff only sets txSwept = true if err != nil (DB error) or if an input has Swept: true. If the input is expired but Swept: false, the output would also get Swept: false.

Tests

Two well-structured e2e tests covering both swept scenarios. Good use of sync.WaitGroup for async NotifyIncomingFunds. Minor nit: typo in existing comment — "proviving" → "providing" (line 1027, pre-existing).

Verdict: The core fix is correct and addresses a real security concern. The "expired but not swept" edge case needs clarification on expected behavior.

Copy link
Copy Markdown
Contributor

@arkanaai arkanaai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — #999

Fix: Finalize swept pending tx by @louisinger


PROTOCOL-CRITICAL — Requires human approval before merge

This PR touches VTXO state management (swept status propagation during offchain tx finalization). Bugs here could cause users to believe they have spendable funds that have already been swept, or conversely mark live VTXOs as swept. Human sign-off is mandatory.


Summary

When a pending offchain transaction is finalized after its inputs have expired (and potentially been swept by the server), the newly created output VTXOs must inherit the Swept flag. Previously, outputs were only marked swept based on dust-script detection. This PR adds logic to check whether the offchain tx is expired at finalization time, and if so, inspects input VTXOs to determine if they were swept.

Correctness

The core logic is sound:

  1. Decode inputs (previously discarded with _) to look up input VTXOs.
  2. If the tx has expired (ExpiryTimestamp > 0 && time.Now().After(...)), fetch input VTXOs and check their Swept flag.
  3. If any input is swept, mark all outputs as swept.
  4. The outputSwept variable correctly preserves the existing dust-script check as a fallback when txSwept is false.

The fix addresses a real gap: without it, a user finalizing a stale pending tx would receive VTXOs that appear spendable but whose underlying funds have already been claimed by the server's sweep.

Security Concerns

1. txSwept = err != nil — error-as-swept assumption (also flagged by CodeRabbit)

This is the most concerning line. If GetVtxos fails for a transient reason (DB timeout, connection blip), all outputs get permanently marked as swept. The comment says "it should never happen" but in production, transient DB errors do happen. This is a conservative-fail approach (better to falsely mark swept than falsely mark spendable), which is arguably the safer direction for fund safety. However, it means a transient DB error could render a user's funds inaccessible until manually corrected.

Recommendation: At minimum, log a warning when this path is taken so operators can detect it. Ideally, return the error and let the caller retry, rather than silently degrading.

2. Time-of-check vs. time-of-use on expiry

The code uses time.Now() to decide whether the tx is expired. In theory the sweep could happen between the time check and the GetVtxos call, but this is a negligible window and the consequence is only that outputs won't be marked swept — the next finalization attempt or a subsequent sweep check would catch it. Acceptable.

3. All-or-nothing sweep propagation

If any input is swept, all outputs are marked swept. This is correct for the Ark protocol because a partially-swept set of inputs means the entire transaction's on-chain anchoring is compromised. The checkpoint tx that commits these outputs depends on all inputs being valid.

Edge Cases

  • Non-expired finalization: The ExpiryTimestamp > 0 && time.Now().After(...) guard correctly skips the sweep check for non-expired txs. Normal flow is unaffected.
  • GetVtxos returns fewer VTXOs than inputs: If some input VTXOs are not found in the DB (the sqlite store skips outpoints with no rows), the returned slice will be shorter than ins. The code only checks inputVtxo.Swept on returned VTXOs, so missing VTXOs are treated as not-swept. This could be a gap — if an input VTXO is missing from the DB entirely, it might indicate it was already consumed/swept. Worth considering whether missing VTXOs should also trigger the swept flag.
  • Dust check override: When txSwept is true, the dust check (IsSubDustScript) is skipped. This is correct — if the tx is swept, all outputs are swept regardless of dust status.

Test Coverage

The E2E tests cover both scenarios described in the PR:

  1. VTXOs already swept: Generates blocks, waits 30s for server sweep, verifies input is swept, finalizes, asserts output is swept. Good.
  2. VTXOs expired but not yet swept: Generates blocks but does NOT wait for server sweep, verifies input is NOT swept at query time, finalizes, asserts output is STILL marked swept.

The second test asserts require.True(t, incomingFunds[0].Swept) even when the input VTXO was verified as require.False(t, resp.Vtxos[0].Swept). This means the code marks outputs as swept even when inputs are not swept, purely based on some condition at finalization time. But looking at the production code, txSwept is only set to true if err != nil OR if inputVtxo.Swept is true. If inputs are not swept and no error occurred, txSwept remains false.

So how does the second test pass? The client-side check happens before finalization, but by the time the server processes it, the sweep may have already occurred (race between client's check and server's processing). This makes the "expired but not swept" test timing-dependent and potentially non-deterministic.

Missing from the PR description's ask: The description requested two test cases, one for "expired but not swept by the server." The existing test may rely on timing rather than deterministically testing the case where inputs are truly not swept at server finalization time.

Verdict

The fix is directionally correct and addresses a real safety gap. The conservative approach of marking outputs swept when in doubt is the right call for fund safety. However:

  1. The err != nil => swept fallback should at least log a warning so operators can detect transient DB failures being silently converted to swept status.
  2. The behavior when GetVtxos returns fewer results than expected inputs deserves explicit handling (missing VTXOs could also indicate swept state).
  3. The "expired but not swept" E2E test may not be deterministically testing what it claims.

This is protocol-critical code. Requesting human review from @altafan before merge.

Comment thread pkg/ark-lib/locktime.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
internal/infrastructure/db/service.go (1)

536-544: ⚠️ Potential issue | 🟠 Major

DB errors should not infer swept status.

The condition txSwept = err != nil || ... at line 543 treats database read failures as if the transaction is swept. While the comment acknowledges this "should never happen," transient DB errors could incorrectly mark spendable vtxos as swept, permanently locking user funds.

Consider handling the error explicitly rather than defaulting to swept:

Proposed fix
 		txSwept := false
 		batch, err := s.roundStore.GetRoundWithCommitmentTxid(ctx, offchainTx.RootCommitmentTxId)
-		// We consider the tx swept if:
-		// - there is an error fetching the batch (this is just fallback, should never happen)
-		// - the batch is swept
-		// - the tx expired (meaning one or all its inputs expired and are already swept or about
-		// to be swept)
-		txSwept = err != nil || (batch != nil && len(batch.SweepTxs) > 0) ||
-			time.Now().After(time.Unix(offchainTx.ExpiryTimestamp, 0))
+		if err != nil {
+			log.WithError(err).Warnf(
+				"failed to fetch batch for offchain tx %s; cannot determine swept state",
+				offchainTx.ArkTxid,
+			)
+			// Do not default to swept on DB errors - fall through with txSwept=false
+		} else if (batch != nil && len(batch.SweepTxs) > 0) ||
+			time.Now().After(time.Unix(offchainTx.ExpiryTimestamp, 0)) {
+			txSwept = true
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/infrastructure/db/service.go` around lines 536 - 544, The code
currently sets txSwept to true when s.roundStore.GetRoundWithCommitmentTxid
returns an error; instead, explicitly handle the error from
GetRoundWithCommitmentTxid (s.roundStore.GetRoundWithCommitmentTxid) rather than
inferring swept status: remove the err != nil clause from the txSwept boolean
expression and add an explicit branch that handles err (e.g., return or
propagate the error, or log and continue treating the TX as not swept), then
compute txSwept only from (batch != nil && len(batch.SweepTxs) > 0) ||
time.Now().After(time.Unix(offchainTx.ExpiryTimestamp, 0)); keep references to
txSwept, batch.SweepTxs, offchainTx.RootCommitmentTxId and
offchainTx.ExpiryTimestamp so the change is localized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/test/e2e/e2e_test.go`:
- Line 1143: Fix the typo in the comment in internal/test/e2e/e2e_test.go:
change the comment text "Givetime to the server to sweep the vtxo" to "Give time
to the server to sweep the vtxo" (update the comment near the relevant test
where the server sweep/wait is documented).

---

Duplicate comments:
In `@internal/infrastructure/db/service.go`:
- Around line 536-544: The code currently sets txSwept to true when
s.roundStore.GetRoundWithCommitmentTxid returns an error; instead, explicitly
handle the error from GetRoundWithCommitmentTxid
(s.roundStore.GetRoundWithCommitmentTxid) rather than inferring swept status:
remove the err != nil clause from the txSwept boolean expression and add an
explicit branch that handles err (e.g., return or propagate the error, or log
and continue treating the TX as not swept), then compute txSwept only from
(batch != nil && len(batch.SweepTxs) > 0) ||
time.Now().After(time.Unix(offchainTx.ExpiryTimestamp, 0)); keep references to
txSwept, batch.SweepTxs, offchainTx.RootCommitmentTxId and
offchainTx.ExpiryTimestamp so the change is localized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2f18c877-9c06-4137-81ac-3320013fbd54

📥 Commits

Reviewing files that changed from the base of the PR and between 088acf5 and 0c3cbe4.

📒 Files selected for processing (9)
  • README.md
  • docker-compose.regtest.yml
  • envs/arkd.dev.env
  • envs/arkd.light.env
  • internal/config/config.go
  • internal/core/application/service.go
  • internal/infrastructure/db/service.go
  • internal/test/e2e/e2e_test.go
  • pkg/ark-lib/locktime.go
💤 Files with no reviewable changes (2)
  • docker-compose.regtest.yml
  • envs/arkd.dev.env

Comment thread internal/test/e2e/e2e_test.go Outdated
Comment thread envs/arkd.light.env Outdated
Comment thread internal/infrastructure/db/service.go
Comment thread internal/core/application/service.go Outdated
Copy link
Copy Markdown
Collaborator

@bitcoin-coder-bob bitcoin-coder-bob left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left some questions about a race condition or overly broad swept marking

@bitcoin-coder-bob bitcoin-coder-bob self-requested a review April 8, 2026 18:53
@bitcoin-coder-bob
Copy link
Copy Markdown
Collaborator

approved if CI passes

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
internal/test/e2e/e2e_test.go (2)

2765-2784: These shorter sleeps are still blind waits around async state.

Each of these sites immediately asserts on indexer or sweeper state after the sleep. Shortening the delay without switching to condition-based waits makes the suite load-sensitive and, in the dust-sweep case, can even pass before the automatic sweep attempt has actually been observed. Prefer polling the specific observable condition at each site instead of relying on fixed delays.

Also applies to: 3132-3145, 3219-3231, 3364-3407, 3467-3479

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/test/e2e/e2e_test.go` around lines 2765 - 2784, Replace the blind
time.Sleep calls with a short polling loop that repeatedly checks the actual
observable condition until a timeout: after wg.Wait() poll alice.ListVtxos(ctx)
and/or indexerSvc.GetVirtualTxs(ctx, []string{res.Txid}) instead of sleeping,
and break when spendable contains an entry with Txid == res.Txid and indexerSvc
returns a non-empty resp.Txs (set virtualTx from resp.Txs[0]); keep
require.NoError checks and fail the test if the timeout elapses. Target the code
using alice.ListVtxos, indexerSvc.GetVirtualTxs, res.Txid and virtualTx when
implementing the polling so other similar blocks can be updated the same way.

1042-1135: Extract the pending-tx setup into a helper.

Both new subtests repeat the same wallet/address/tapscript/build/sign/submit flow, and that same sequence already exists in the preceding finalize pending tx case. A helper would keep these tests focused on the sweep/expiry behavior and reduce copy/paste drift.

Also applies to: 1187-1280

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/test/e2e/e2e_test.go` around lines 1042 - 1135, The tests duplicate
the wallet/address/tapscript/build/sign/submit sequence; extract that repeated
setup into a helper (e.g., preparePendingOffchainTx or setupPendingTxForSweep)
that accepts (t *testing.T, ctx context.Context, explorer string) or similar and
performs the steps currently done with setupArkSDKwithPublicKey, faucetOffchain,
alice.FinalizePendingTxs, aliceWallet.GetAddresses, script.ParseVtxoScript,
vtxoScript.ForfeitClosures, closure.Script, vtxoTapTree.GetTaprootMerkleProof,
txscript.ParseControlBlock, build the tapscript, decode CheckpointTapscript,
BuildTxs, checkpoint encoding, ptx.B64Encode and aliceWallet.SignTransaction
then arkSvc.SubmitTx; have the helper return the minimal values callers need
(e.g., txid, signedArkTx, encodedCheckpoints, alice/arkSvc cleanup functions or
wallets if needed). Replace the duplicated blocks in the two subtests (and the
earlier finalize pending tx case) with calls to that helper to keep tests
focused on sweep/expiry behavior and avoid copy/paste drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/test/e2e/e2e_test.go`:
- Around line 1139-1155: The test uses a fixed time.Sleep after
generateBlocks(21) and then asserts transient vtxo states, which is race-prone;
replace the hard sleep and direct assertions with polling using
require.Eventually (or a similar retry loop) around alice.Indexer().GetVtxos to
wait for the expected final state (Swept==true) and, where you need to observe
the transient Spent==true && Swept==false window, add a test hook to the sweeper
(or a way to pause/observe it) and use require.Eventually to assert that
transient state while the sweeper is paused; specifically modify the blocks
around generateBlocks, time.Sleep, and the alice.Indexer().GetVtxos checks to
poll for the desired states instead of relying on fixed sleeps.

---

Nitpick comments:
In `@internal/test/e2e/e2e_test.go`:
- Around line 2765-2784: Replace the blind time.Sleep calls with a short polling
loop that repeatedly checks the actual observable condition until a timeout:
after wg.Wait() poll alice.ListVtxos(ctx) and/or indexerSvc.GetVirtualTxs(ctx,
[]string{res.Txid}) instead of sleeping, and break when spendable contains an
entry with Txid == res.Txid and indexerSvc returns a non-empty resp.Txs (set
virtualTx from resp.Txs[0]); keep require.NoError checks and fail the test if
the timeout elapses. Target the code using alice.ListVtxos,
indexerSvc.GetVirtualTxs, res.Txid and virtualTx when implementing the polling
so other similar blocks can be updated the same way.
- Around line 1042-1135: The tests duplicate the
wallet/address/tapscript/build/sign/submit sequence; extract that repeated setup
into a helper (e.g., preparePendingOffchainTx or setupPendingTxForSweep) that
accepts (t *testing.T, ctx context.Context, explorer string) or similar and
performs the steps currently done with setupArkSDKwithPublicKey, faucetOffchain,
alice.FinalizePendingTxs, aliceWallet.GetAddresses, script.ParseVtxoScript,
vtxoScript.ForfeitClosures, closure.Script, vtxoTapTree.GetTaprootMerkleProof,
txscript.ParseControlBlock, build the tapscript, decode CheckpointTapscript,
BuildTxs, checkpoint encoding, ptx.B64Encode and aliceWallet.SignTransaction
then arkSvc.SubmitTx; have the helper return the minimal values callers need
(e.g., txid, signedArkTx, encodedCheckpoints, alice/arkSvc cleanup functions or
wallets if needed). Replace the duplicated blocks in the two subtests (and the
earlier finalize pending tx case) with calls to that helper to keep tests
focused on sweep/expiry behavior and avoid copy/paste drift.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4ab1b500-9bc0-472d-8674-a1ee07e6ace1

📥 Commits

Reviewing files that changed from the base of the PR and between 0c3cbe4 and eb676b4.

📒 Files selected for processing (2)
  • internal/core/application/service.go
  • internal/test/e2e/e2e_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/core/application/service.go

Comment thread internal/test/e2e/e2e_test.go
@arkanaai
Copy link
Copy Markdown
Contributor

arkanaai bot commented Apr 9, 2026

This PR is approved — ready to merge?

Comment thread internal/core/application/service.go Outdated
).WithMetadata(errors.VtxoMetadata{VtxoOutpoint: vtxoOutpoint})
}
if vtxo.Swept || !s.sweeper.scheduler.AfterNow(vtxo.ExpiresAt) {
if vtxo.Swept || now.After(time.Unix(vtxo.ExpiresAt, 0)) {
Copy link
Copy Markdown
Collaborator Author

@louisinger louisinger Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if vtxo.Swept || now.After(time.Unix(vtxo.ExpiresAt, 0)) {
if vtxo.Swept || vtxo.IsExpired() {

let's move this logic to vtxo type so we can reuse it ? also cleaner to read

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in 14fb4c7

Comment thread internal/config/config.go Outdated
@altafan altafan merged commit 90ee4eb into arkade-os:master Apr 10, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants